package com.fast.common.redis;

import cn.hutool.core.map.MapUtil;
import com.alibaba.nacos.common.utils.CollectionUtils;
import com.alibaba.nacos.shaded.com.google.common.collect.Lists;
import com.alibaba.nacos.shaded.com.google.common.collect.Sets;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.*;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static org.springframework.data.redis.connection.RedisStringCommands.SetOption.UPSERT;


@AllArgsConstructor
@Slf4j
@Service
public class RedisClient {

    private final RedisTemplate redisTemplate;
    private final RedissonClient redissonClient;

    /**
     * 其他方法参见注释
     * common value 方法无特殊标记
     * 针对所有的hash 都是以h开头的方法
     * 针对所有的Set 都是以s开头的方法
     * 针对所有的zSet 都是以zs开头的方法
     * 针对所有的List 都是以l开头的方法
     * {@link RedisClient }
     */
    // ====================== common methods =========================================
    public Object get(String key) {
        try {
            return redisTemplate.opsForValue().get(key);
        } catch (Exception e) {
            log.error("redis get error", e);
            return null;
        }
    }

    public String getAsString(String key) {
        try {
            return (String) redisTemplate.opsForValue().get(key);
        } catch (Exception e) {
            log.error("redis getAsString error", e);
            return null;
        }
    }

    public void setString(String key, String value) {
        try {
            redisTemplate.opsForValue().set(key, value);
        } catch (Exception e) {
            log.error("redis setString error", e);
        }
    }

    public void setString(String key, String value, long time, TimeUnit timeUnit) {
        try {
            Assert.isTrue(time > 0, "过期时间不能小于0");
            redisTemplate.opsForValue().set(key, value, time, timeUnit);
        } catch (Exception e) {
            log.error("redis setString error", e);
        }
    }

    public boolean hasKey(Object key) {
        try {
            Boolean result = redisTemplate.hasKey(key);
            return result == null ? Boolean.FALSE : result.booleanValue();
        } catch (Exception e) {
            log.error("redis hasKey error", e);
            return false;
        }
    }

    public void set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
        } catch (Exception e) {
            log.error("redis set error", e);
        }
    }

    public void set(String key, Object value, long time, TimeUnit timeUnit) {
        try {
            Assert.isTrue(time > 0, "过期时间不能小于0");
            redisTemplate.opsForValue().set(key, value, time, timeUnit);
        } catch (Exception e) {
            log.error("redis set error", e);
        }
    }

    public boolean setNX(String key, Object value, long time, TimeUnit timeUnit) {
        Assert.isTrue(time > 0, "过期时间不能小于0");
        try {
            return redisTemplate.opsForValue().setIfAbsent(key, value, time, timeUnit);
        } catch (Exception e) {
            log.error("redis setNX error", e);
            throw new RuntimeException(e.getMessage());
        }
    }

    public boolean expire(String key, long time, TimeUnit timeUnit) {
        try {
            Boolean result = redisTemplate.expire(key, time, timeUnit);
            return result == null ? Boolean.FALSE : result.booleanValue();
        } catch (Exception e) {
            log.error("redis expire error", e);
            return false;
        }
    }

    public void delete(Object key) {
        try {
            if (key instanceof Collection) {
                redisTemplate.delete((Collection) key);
            } else {
                redisTemplate.delete(key);
            }
        } catch (Exception e) {
            log.error("redis delete error", e);
        }
    }

    public Long increment(Object key) {
        try {
            return redisTemplate.opsForValue().increment(key);
        } catch (Exception e) {
            log.error("redis increment error", e);
            return 0L;
        }
    }

    public void decrement(Object key) {
        try {
            redisTemplate.opsForValue().decrement(key);
        } catch (Exception e) {
            log.error("redis decrement error", e);
        }
    }

    public void setBoundValue(String key, Object value) {
        try {
            redisTemplate.boundValueOps(key).set(value);
        } catch (Exception e) {
            log.error("redis setBoundValue error", e);
        }
    }
    // ====================== common methods =========================================

    public Object getBoundValue(String key) {
        try {
            return redisTemplate.boundValueOps(key).get();
        } catch (Exception e) {
            log.error("redis getBoundValue error", e);
            return null;
        }
    }

    //================================ hash methods =========================================
    public Object hGet(Object key, Object hashKey) {
        try {
            return redisTemplate.opsForHash().get(key, hashKey);
        } catch (Exception e) {
            log.error("redis hGet error", e);
            return null;
        }
    }

    public void hPut(String key, Object hashKey, Object value) {
        try {
            redisTemplate.opsForHash().put(key, hashKey, value);
        } catch (Exception e) {
            log.error("redis hPut error", e);
        }
    }

    public void hPutAll(String key, Map data) {
        try {
            redisTemplate.opsForHash().putAll(key, data);
        } catch (Exception e) {
            log.error("redis hPut error", e);
        }
    }

    public boolean hHasKey(Object key, Object hashKey) {
        try {
            Boolean result = redisTemplate.opsForHash().hasKey(key, hashKey);
            return result == null ? Boolean.FALSE : result.booleanValue();
        } catch (Exception e) {
            log.error("redis hHasKey error", e);
            return false;
        }
    }

    public Set hKeys(Object key) {
        try {
            return redisTemplate.opsForHash().keys(key);
        } catch (Exception e) {
            log.error("redis hKeys error", e);
            return new HashSet();
        }
    }

    public List hValues(Object key) {
        try {
            return redisTemplate.opsForHash().values(key);
        } catch (Exception e) {
            log.error("redis hValues error", e);
            return new ArrayList();
        }
    }

    public void hDelete(String key, String... hashKeys) {
        try {
            redisTemplate.opsForHash().delete(key, hashKeys);
        } catch (Exception e) {
            log.error("redis hDelete error", e);
        }
    }

    public long hSize(String key) {
        try {
            Long size = redisTemplate.opsForHash().size(key);
            return size == null ? 0L : size.longValue();
        } catch (Exception e) {
            log.error("redis hSize error", e);
            return 0L;
        }
    }

    public void hPutAll(Object key, Map map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
        } catch (Exception e) {
            log.error("redis hPutAll error", e);
        }
    }

    public Map hGetMap(String key) {
        try {
            if (StringUtils.isBlank(key)) {
                return null;
            }
            return redisTemplate.opsForHash().entries(key);
        } catch (Exception e) {
            log.error("redis hGetMap error", e);
            return null;
        }
    }


    //================================ hash methods =========================================

    public void hIncrement(Object key, Object hashKey) {
        try {
            redisTemplate.opsForHash().increment(key, hashKey, 1L);
        } catch (Exception e) {
            log.error("redis hIncrement error", e);
        }
    }

    //================================ set methods =========================================
    public void sAdd(String key, Object... values) {
        try {
            redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            log.error("redis sAdd error", e);
        }
    }

    public void sAddForString(String key, String... values) {
        try {
            redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            log.error("redis sAdd error", e);
        }
    }

    public void sRemove(Object key, Object... values) {
        try {
            redisTemplate.opsForSet().remove(key, values);
        } catch (Exception e) {
            log.error("redis sRemove error", e);
        }
    }

    public Set sMembers(Object key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            log.error("redis sMembers error", e);
            return Sets.newHashSet();
        }
    }


    public Object sRandomMembers(Object key) {
        try {
            return redisTemplate.opsForSet().randomMember(key);
        } catch (Exception e) {
            log.error("redis sMembers error", e);
            return null;
        }
    }

    public long sSize(Object key) {
        try {
            Long size = redisTemplate.opsForSet().size(key);
            return size == null ? 0L : size;
        } catch (Exception e) {
            log.error("redis sSize error", e);
            return 0L;
        }
    }

    public List sPop(Object key, long count) {
        try {
            return redisTemplate.opsForSet().pop(key, count);
        } catch (Exception e) {
            log.error("redis sPop error", e);
            return Lists.newArrayList();
        }
    }

    //================================ set methods =========================================

    //================================ list methods =========================================

    public List<String> sPopForString(String key, long count) {
        try {
            return redisTemplate.opsForSet().pop(key, count);
        } catch (Exception e) {
            log.error("redis sPopForString error", e);
            return Lists.newArrayList();
        }
    }

    /**
     * rightPush
     *
     * @param key
     * @param value
     * @return if success return true
     */
    public boolean lRightPush(Object key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            log.error("redis lRightPushAll error", e);
            return false;
        }
    }

    public boolean lRightPushAll(Object key, Collection values) {
        try {
            redisTemplate.opsForList().rightPushAll(key, values);
            return true;
        } catch (Exception e) {
            log.error("redis lRightPushAll error", e);
            return false;
        }
    }

    public Object lLeftPop(Object key) {
        try {
            return redisTemplate.opsForList().leftPop(key);
        } catch (Exception e) {
            log.error("redis lRightPushAll error", e);
            return null;
        }
    }

    public List lLeftPop(Object key, int count) {
        try {
            byte[] keyByte = redisTemplate.getKeySerializer().serialize(key);
            List<Object> result = redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
                IntStream.range(0, count).forEach(i -> {
                    connection.lPop(keyByte);
                });
                return null;
            });
            if (CollectionUtils.isNotEmpty(result)) {
                result = result.stream().filter(a -> a != null).collect(Collectors.toList());
            }
            return result;
        } catch (Exception e) {
            log.error("redis lLeftPop error", e);
            throw new RuntimeException("redis lLeftPop error, " + e.getMessage());
        }
    }

    public long lSize(Object key) {
        try {
            Long size = redisTemplate.opsForList().size(key);
            return size == null ? 0L : size.longValue();
        } catch (Exception e) {
            log.error("redis lSize error", e);
            return 0L;
        }
    }

    public List lRange(Object key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            log.error("redis lRange error", e);
            return Lists.newArrayList();
        }
    }

    @Transactional
    public void lRemove(Object key, Object value) {
        lRemove(key, 1, value);
    }

    //================================ list methods =========================================

    @Transactional
    public void lRemove(Object key, long count, Object value) {
        try {
            redisTemplate.opsForList().remove(key, count, value);
        } catch (Exception e) {
            log.error("redis lRange error", e);
        }
    }

    //================================ zset methods =========================================
    public boolean zAdd(String key, Object value, double score) {
        try {
            return redisTemplate.opsForZSet().add(key, value, score);
        } catch (Exception e) {
            log.error("redis zAdd error", e);
            return false;
        }
    }

    public void zAddAll(String key, Collection values, double score) {
        try {
            byte[] keyByte = redisTemplate.getKeySerializer().serialize(key);
            RedisSerializer valueSerializer = redisTemplate.getValueSerializer();
            redisTemplate.executePipelined((RedisCallback<Boolean>) connection -> {
                values.forEach(value -> connection.zAdd(keyByte, score, valueSerializer.serialize(value)));
                return null;
            });
        } catch (Exception e) {
            log.error("redis zAddAll error", e);
            throw new RuntimeException("redis zAddAll error, " + e.getMessage());
        }
    }

    public void zAddAll(String key, Collection values, Map<String, Double> scoreMap) {
        try {
            byte[] keyByte = redisTemplate.getKeySerializer().serialize(key);
            RedisSerializer valueSerializer = redisTemplate.getValueSerializer();
            redisTemplate.executePipelined((RedisCallback<Boolean>) connection -> {
                values.forEach(value -> connection.zAdd(keyByte, MapUtil.getDouble(scoreMap, value, 0d), valueSerializer.serialize(value)));
                return null;
            });
        } catch (Exception e) {
            log.error("redis zAddAll error", e);
            throw new RuntimeException("redis zAddAll error, " + e.getMessage());
        }
    }

    public void zIncrement(String key, String value, double score) {
        try {
            redisTemplate.opsForZSet().incrementScore(key, value, score);
        } catch (Exception e) {
            log.error("redis zAdd error", e);
        }
    }

    public long zSize(String key) {
        try {
            return redisTemplate.opsForZSet().size(key);
        } catch (Exception e) {
            log.error("redis zSize error", e);
            return 0L;
        }
    }

    public Set zRange(Object key, long start, long end) {
        try {
            Set zrangeWithScores = redisTemplate.opsForZSet().rangeWithScores(key, start, end);
            log.info("redis range:" + zrangeWithScores.toString());
            return redisTemplate.opsForZSet().range(key, start, end);
        } catch (Exception e) {
            log.error("redis zRange error", e);
            return new HashSet();
        }
    }

    public Set zRevRange(String key, long start, long end) {
        try {
            Set zrangeWithScores = redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
            log.info("redis range:" + zrangeWithScores.toString());

            return redisTemplate.opsForZSet().reverseRange(key, start, end);
        } catch (Exception e) {
            log.error("redis zRevRange error", e);
            return new HashSet();
        }
    }

    public boolean zRemove(Object key, Object... values) {
        try {
            if (values == null || values.length == 0) {
                return true;
            }
            long size = redisTemplate.opsForZSet().remove(key, values);
            return values.length == size;
        } catch (Exception e) {
            log.error("redis sMembers error", e);
            return false;
        }
    }

    public boolean zRemoveAll(Object key, Set values) {
        try {
            if (CollectionUtils.isEmpty(values)) {
                return true;
            }
            long size = redisTemplate.opsForZSet().remove(key, values.toArray(new Object[0]));
            return values.size() == size;
        } catch (Exception e) {
            log.error("redis sMembers error", e);
            return false;
        }
    }

    //-------------------------- others --------------------------
    public void publish(String channel, String message) {
        redisTemplate.convertAndSend(channel, message);
    }

    public void multiSet(List<String> keys, Object value, long time, TimeUnit timeUnit) {
        if (CollectionUtils.isEmpty(keys)) {
            return;
        }
        try {
            List<byte[]> keyBytes = keys.stream().map(key -> redisTemplate.getKeySerializer().serialize(key)).collect(Collectors.toList());
            RedisSerializer valueSerializer = redisTemplate.getValueSerializer();
            redisTemplate.executePipelined((RedisCallback<Boolean>) connection -> {
                keyBytes.forEach(keyByte -> {
                    connection.set(keyByte, valueSerializer.serialize(value), Expiration.from(time, timeUnit), UPSERT);
                });
                return null;
            });
        } catch (Exception e) {
            log.error("redis multiSet error", e);
            throw new RuntimeException("redis multiSet error, " + e.getMessage());
        }
    }


    //-------------------------- redisson --------------------------
    public RLock getLock(String name){
        return redissonClient.getLock(name);
    }

    public RReadWriteLock getReadWriteLock(String name){
        return redissonClient.getReadWriteLock(name);
    }

    public RRateLimiter getRateLimiter(String name) {
        return redissonClient.getRateLimiter(name);
    }

    public RSemaphore getSemaphore(String name) {
        return redissonClient.getSemaphore(name);
    }
}
